5  What are transforms?

5.1 Motivation

In Part I of the book, we learned that our GeoTable representation of geospatial data provides the data access pattern of the DataFrame, a feature that is very convenient for data science. To recap, let’s consider the following geotable with four random variables:

N = 10000
a = [2randn(N÷2) .+ 6; randn(N÷2)]
b = [3randn(N÷2); 2randn(N÷2)]
c = randn(N)
d = c .+ 0.6randn(N)

table = (; a, b, c, d)

gt = georef(table, CartesianGrid(100, 100))
10000×5 GeoTable over 100×100 CartesianGrid
a b c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 0.556507 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 2.10409 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 -0.215347 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 0.739401 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 -0.984792 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 0.245692 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 -1.30648 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 -2.20715 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 -0.442907 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 -0.429084 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

We can easily retrieve the “a” column of the geotable as a vector, and plot its histogram:

Mke.hist(gt.a, color = "gray80")
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

We can compute the cross-correlation between columns “a” and “b”:

cor(gt.a, gt.b)
0.003120474427862065

And inspect bivariate distributions of the values of the geotable with PairPlots.jl by Thompson (2023):

using PairPlots

pairplot(values(gt))
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

This pattern is useful to answer geoscientific questions via marginal analysis (i.e. entire columns treated as measurements of a single random variable). However, the answers to many questions in geosciences depend on where the measurements were made.

Attempting to answer geoscientific questions with basic access to rows and columns can be very frustrating. In particular, this approach is prone to unintentional removal of geospatial information:

gt.a
10000-element Vector{Float64}:
  6.167217484380527
  4.910277381566566
  3.574818244983638
  8.496198468255605
  3.6242176072701784
  5.942066287078733
  5.5361232946064955
  6.385170207370261
  5.901303332434973
  4.891416008712465
  6.368172527651614
  9.009302664823702
  6.757465422855121
  ⋮
  1.1660244039654948
 -0.33330999131741645
 -0.5744351471617439
 -0.8271780157894039
  0.07300104409138085
  0.0876566830075012
  2.218504020619291
 -0.9099659170219968
 -0.011401261593054829
  0.3003998209631712
 -0.4380025503033772
  2.0623104674412827
Note

Any script that is written in terms of direct column access has the potential to discard the special geometry column, and become unreadable very quickly with the use of auxiliary indices for rows.

We propose a new approach to geospatial data science with the concept of transforms, which we introduce in three classes with practical examples:

  1. Feature transforms
  2. Geometric transforms
  3. Geospatial transforms

5.2 Feature transforms

A feature transform is a function that takes the values of the geotable and produces a new set of values over the same geospatial domain. The framework provides over 30 such transforms, ranging from basic selection of columns, to data cleaning, to advanced multivariate statistical transforms.

5.2.1 Basic

Let’s start with two basic and important transforms, Select and Reject. The Select transform can be used to select columns of interest from a geotable:

gt |> Select("a", "b") # select columns "a" and "b"
10000×3 GeoTable over 100×100 CartesianGrid
a b geometry
Continuous Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

In the example above, we selected the columns “a” and “b” explicitly, but Select has various methods for more flexible column selection:

gt |> Select(1:3) # select columns 1 to 3
10000×4 GeoTable over 100×100 CartesianGrid
a b c geometry
Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))
gt |> Select(r"[bcd]") # columns matching regular expression
10000×4 GeoTable over 100×100 CartesianGrid
b c d geometry
Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
-2.93955 0.801095 0.556507 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
1.31071 2.21353 2.10409 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
-3.56032 0.238011 -0.215347 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
-2.08972 0.774768 0.739401 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
1.44806 -0.446267 -0.984792 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
-3.12735 0.497818 0.245692 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
2.9944 -0.909221 -1.30648 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
1.6265 -1.83513 -2.20715 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
-3.72011 -1.37022 -0.442907 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.76016 -0.947532 -0.429084 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

A convenient method is also provided to select and rename columns:

gt |> Select("a" => "A", "b" => "B")
10000×3 GeoTable over 100×100 CartesianGrid
A B geometry
Continuous Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

The Reject transform can be used to reject columns from a geotable that are not relevant for a given analysis. It supports the same column specification of Select:

gt |> Reject("b") # reject column "b"
10000×4 GeoTable over 100×100 CartesianGrid
a c d geometry
Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 0.801095 0.556507 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 2.21353 2.10409 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 0.238011 -0.215347 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 0.774768 0.739401 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 -0.446267 -0.984792 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 0.497818 0.245692 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 -0.909221 -1.30648 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 -1.83513 -2.20715 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -1.37022 -0.442907 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 -0.947532 -0.429084 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))
Note

Unlike direct column access, the Select and Reject transforms preserve geospatial information.

Tip for all users

The Select transform can be used in conjunction with the viewer to quickly visualize a specific variable:

gt |> Select("a") |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

The Rename transform can be used to rename specific columns of a geotable. It preserves all other columns that are not part of the column specification:

gt |> Rename("a" => "A", "b" => "B")
10000×5 GeoTable over 100×100 CartesianGrid
A B c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 0.556507 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 2.10409 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 -0.215347 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 0.739401 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 -0.984792 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 0.245692 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 -1.30648 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 -2.20715 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 -0.442907 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 -0.429084 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

The Identity transform can be used as a placeholder to forward the geotable without modifications to the next transform:

gt |> Identity()
10000×5 GeoTable over 100×100 CartesianGrid
a b c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 0.556507 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 2.10409 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 -0.215347 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 0.739401 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 -0.984792 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 0.245692 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 -1.30648 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 -2.20715 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 -0.442907 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 -0.429084 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

The RowTable and ColTable transforms change the underlying table representation of the values of the geotable as discussed in the first chapter of the book:

rt = gt |> RowTable()
10000×5 GeoTable over 100×100 CartesianGrid
a b c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 0.556507 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 2.10409 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 -0.215347 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 0.739401 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 -0.984792 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 0.245692 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 -1.30648 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 -2.20715 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 -0.442907 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 -0.429084 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))
rt |> values |> typeof
Vector{@NamedTuple{a::Float64, b::Float64, c::Float64, d::Float64}} (alias for Array{@NamedTuple{a::Float64, b::Float64, c::Float64, d::Float64}, 1})

The Functional transform can be used to apply a function to columns of a geotable in place:

gt |> Functional(cos) |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238
gt |> Functional("a" => cos, "b" => sin) |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

The Map transform can be used to create new columns from existing columns in the geotable. It takes a column specification, calls a function on the selected columns row-by-row, and returns the result as a new column:

gt |> Map("a" => sin, "b" => cos => "cos(b)")
10000×7 GeoTable over 100×100 CartesianGrid
a b c d sin_a cos(b) geometry
Continuous Continuous Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 0.556507 -0.115708 -0.979658 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 2.10409 -0.980484 0.257166 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 -0.215347 -0.419801 -0.913608 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 0.739401 0.80077 -0.495942 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 -0.984792 -0.464106 0.122424 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 0.245692 -0.334542 -0.999899 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 -1.30648 -0.679486 -0.989186 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 -2.20715 0.101808 -0.05567 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 -0.442907 -0.372668 -0.837274 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 -0.429084 -0.984017 0.0477556 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))
gt |> Map([2, 3] => ((b, c) -> 2b + c) => "f(b, c)")
10000×6 GeoTable over 100×100 CartesianGrid
a b c d f(b, c) geometry
Continuous Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 0.801095 0.556507 -5.078 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 2.21353 2.10409 4.83494 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 0.238011 -0.215347 -6.88263 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 0.774768 0.739401 -3.40466 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 -0.446267 -0.984792 2.44986 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 0.497818 0.245692 -5.75687 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 -0.909221 -1.30648 5.07957 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 -1.83513 -2.20715 1.41786 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 -1.37022 -0.442907 -8.81044 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 -0.947532 -0.429084 8.57279 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

The name of the resulting column can be provided or omitted. If the name is omitted like in the example above with the column “a”, it is created by concatenation of column and function names.

Note

The Map transform mimics the behavior of the transform function in DataFrames.jl, except that it always broadcasts the functions to the rows of the selected columns and always produces a single column for each function.

To filter rows in the geotable based on a given predicate (i.e., a function that returns true or false), we can use the Filter transform:

gt |> Filter(row -> row.a < 0 && row.b > 0)
1185×5 GeoTable over 1185 view(::CartesianGrid, [719, 2263, 5005, 5011, ..., 9985, 9991, 9997, 9999])
a b c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
-0.114779 3.99115 0.869878 0.230587 Quadrangle((x: 18.0 m, y: 7.0 m), ..., (x: 18.0 m, y: 8.0 m))
-1.4779 5.09156 -0.107233 -0.424456 Quadrangle((x: 62.0 m, y: 22.0 m), ..., (x: 62.0 m, y: 23.0 m))
-0.07477 0.132503 0.559919 0.364382 Quadrangle((x: 4.0 m, y: 50.0 m), ..., (x: 4.0 m, y: 51.0 m))
-0.659248 0.903124 0.562797 0.114109 Quadrangle((x: 10.0 m, y: 50.0 m), ..., (x: 10.0 m, y: 51.0 m))
-0.702421 0.354697 0.0874524 0.637321 Quadrangle((x: 19.0 m, y: 50.0 m), ..., (x: 19.0 m, y: 51.0 m))
-1.83957 1.01502 -0.160337 -0.0370398 Quadrangle((x: 29.0 m, y: 50.0 m), ..., (x: 29.0 m, y: 51.0 m))
-1.06785 0.872782 0.403032 0.356063 Quadrangle((x: 37.0 m, y: 50.0 m), ..., (x: 37.0 m, y: 51.0 m))
-1.01172 2.06437 -1.05428 -1.51154 Quadrangle((x: 38.0 m, y: 50.0 m), ..., (x: 38.0 m, y: 51.0 m))
-0.183608 2.34302 -0.394598 -0.895643 Quadrangle((x: 41.0 m, y: 50.0 m), ..., (x: 41.0 m, y: 51.0 m))
-0.85172 2.29273 -0.0767691 0.518739 Quadrangle((x: 45.0 m, y: 50.0 m), ..., (x: 45.0 m, y: 51.0 m))

To sort rows based on specific columns we can use the Sort transform:

gt |> Sort("a", "b")
10000×5 GeoTable over 10000 view(::CartesianGrid, [8486, 9686, 7956, 8457, ..., 1780, 991, 442, 4067])
a b c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
-3.65899 -1.30915 1.54247 0.840452 Quadrangle((x: 85.0 m, y: 84.0 m), ..., (x: 85.0 m, y: 85.0 m))
-3.60801 1.55161 -2.28742 -2.19276 Quadrangle((x: 85.0 m, y: 96.0 m), ..., (x: 85.0 m, y: 97.0 m))
-3.50772 -2.31388 0.810103 1.55905 Quadrangle((x: 55.0 m, y: 79.0 m), ..., (x: 55.0 m, y: 80.0 m))
-3.37621 -1.2281 0.959575 1.28308 Quadrangle((x: 56.0 m, y: 84.0 m), ..., (x: 56.0 m, y: 85.0 m))
-3.25436 -0.632394 -1.15866 -1.08491 Quadrangle((x: 60.0 m, y: 88.0 m), ..., (x: 60.0 m, y: 89.0 m))
-3.19407 -0.537696 0.509656 0.290132 Quadrangle((x: 97.0 m, y: 89.0 m), ..., (x: 97.0 m, y: 90.0 m))
-3.18299 1.40552 0.0753242 0.625766 Quadrangle((x: 43.0 m, y: 61.0 m), ..., (x: 43.0 m, y: 62.0 m))
-3.11663 -2.8021 -1.59764 -1.81342 Quadrangle((x: 77.0 m, y: 92.0 m), ..., (x: 77.0 m, y: 93.0 m))
-3.01928 -0.761217 -0.974547 -0.949558 Quadrangle((x: 70.0 m, y: 74.0 m), ..., (x: 70.0 m, y: 75.0 m))
-2.96669 -2.17923 -0.796896 -0.14077 Quadrangle((x: 89.0 m, y: 89.0 m), ..., (x: 89.0 m, y: 90.0 m))

This transform accepts all options of the sortperm function in Julia, including the option to sort in reverse order:

gt |> Sort("a", "b", rev=true)
10000×5 GeoTable over 10000 view(::CartesianGrid, [4067, 442, 991, 1780, ..., 8457, 7956, 9686, 8486])
a b c d geometry
Continuous Continuous Continuous Continuous Quadrangle
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
12.8392 -0.8186 0.401593 -0.157026 Quadrangle((x: 66.0 m, y: 40.0 m), ..., (x: 66.0 m, y: 41.0 m))
12.4595 -4.03811 1.35602 1.42352 Quadrangle((x: 41.0 m, y: 4.0 m), ..., (x: 41.0 m, y: 5.0 m))
12.3454 -0.660557 0.996428 0.883649 Quadrangle((x: 90.0 m, y: 9.0 m), ..., (x: 90.0 m, y: 10.0 m))
12.3145 2.84629 1.4233 1.40801 Quadrangle((x: 79.0 m, y: 17.0 m), ..., (x: 79.0 m, y: 18.0 m))
12.1136 3.15422 -0.525784 -0.119483 Quadrangle((x: 91.0 m, y: 39.0 m), ..., (x: 91.0 m, y: 40.0 m))
11.9202 2.96111 0.560273 1.50218 Quadrangle((x: 32.0 m, y: 38.0 m), ..., (x: 32.0 m, y: 39.0 m))
11.9157 -0.906479 -0.507113 0.846443 Quadrangle((x: 63.0 m, y: 16.0 m), ..., (x: 63.0 m, y: 17.0 m))
11.8616 -0.904033 0.186772 0.683317 Quadrangle((x: 52.0 m, y: 33.0 m), ..., (x: 52.0 m, y: 34.0 m))
11.7441 -1.48124 -1.0181 -1.57689 Quadrangle((x: 9.0 m, y: 19.0 m), ..., (x: 9.0 m, y: 20.0 m))
11.7282 3.43145 -0.573193 -0.674615 Quadrangle((x: 20.0 m, y: 3.0 m), ..., (x: 20.0 m, y: 4.0 m))

5.2.2 Cleaning

Some feature transforms are used to clean the data before geostatistical analysis. For example, the StdNames transform can be used to standardize variable names that are not very readable due to file format limitations. To illustrate this transform, let’s create a geotable with unreadable variable names:

ut = gt |> Select("a" => "aBc De-F", "b" => "b_2 (1)")
10000×3 GeoTable over 100×100 CartesianGrid
aBc De-F b_2 (1) geometry
Continuous Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

We can standardize the names with:

ut |> StdNames()
10000×3 GeoTable over 100×100 CartesianGrid
ABC_DE_F B_2_1 geometry
Continuous Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

By default the transform, uses the :uppersnake naming convention. Other conventions can be specified depending on personal preference:

ut |> StdNames(:uppercamel)
10000×3 GeoTable over 100×100 CartesianGrid
AbcDeF B21 geometry
Continuous Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))
ut |> StdNames(:upperflat)
10000×3 GeoTable over 100×100 CartesianGrid
ABCDEF B21 geometry
Continuous Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
6.16722 -2.93955 Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 1.0 m))
4.91028 1.31071 Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 1.0 m))
3.57482 -3.56032 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 1.0 m))
8.4962 -2.08972 Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 1.0 m))
3.62422 1.44806 Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 1.0 m))
5.94207 -3.12735 Quadrangle((x: 5.0 m, y: 0.0 m), ..., (x: 5.0 m, y: 1.0 m))
5.53612 2.9944 Quadrangle((x: 6.0 m, y: 0.0 m), ..., (x: 6.0 m, y: 1.0 m))
6.38517 1.6265 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 1.0 m))
5.9013 -3.72011 Quadrangle((x: 8.0 m, y: 0.0 m), ..., (x: 8.0 m, y: 1.0 m))
4.89142 4.76016 Quadrangle((x: 9.0 m, y: 0.0 m), ..., (x: 9.0 m, y: 1.0 m))

The Replace transform can be used to replace specific values in the geotable by new values that are meaningful to the analysis. For example, we can replace the values -999 and NaN that are used to represent missing values in some file formats:

rt = georef((a=[1,-999,3], b=[NaN,5,6]))
3×3 GeoTable over 3 CartesianGrid
a b geometry
Categorical Continuous Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 NaN Segment((x: 0.0 m), (x: 1.0 m))
-999 5.0 Segment((x: 1.0 m), (x: 2.0 m))
3 6.0 Segment((x: 2.0 m), (x: 3.0 m))
rt |> Replace(-999 => missing, NaN => missing)
3×3 GeoTable over 3 CartesianGrid
a b geometry
Categorical Continuous Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 missing Segment((x: 0.0 m), (x: 1.0 m))
missing 5.0 Segment((x: 1.0 m), (x: 2.0 m))
3 6.0 Segment((x: 2.0 m), (x: 3.0 m))

or replace all negative values using a predicate function:

rt |> Replace(<(0) => missing)
3×3 GeoTable over 3 CartesianGrid
a b geometry
Categorical Continuous Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 NaN Segment((x: 0.0 m), (x: 1.0 m))
missing 5.0 Segment((x: 1.0 m), (x: 2.0 m))
3 6.0 Segment((x: 2.0 m), (x: 3.0 m))
Note

In Julia, the expression <(0) is equivalent to the predicate function x -> x < 0.

Although Replace could be used to replace missing values by new values, there is a specific transform for this purpose named Coalesce:

ct = georef((a=[1,missing,3], b=[4,5,6])) |> Coalesce(value=2)
3×3 GeoTable over 3 CartesianGrid
a b geometry
Categorical Categorical Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 4 Segment((x: 0.0 m), (x: 1.0 m))
2 5 Segment((x: 1.0 m), (x: 2.0 m))
3 6 Segment((x: 2.0 m), (x: 3.0 m))
Note

Unlike Replace, the Coalesce transform also changes the column type to make sure that no missing values can be stored in the future:

typeof(ct.a)
Vector{Int64} (alias for Array{Int64, 1})

In many applications, it is enough to simply drop all rows for which the selected column values are missing. This is the purpose of the DropMissing transform:

georef((a=[1,missing,3], b=[4,5,6])) |> DropMissing()
2×3 GeoTable over 2 view(::CartesianGrid, [1, 3])
a b geometry
Categorical Categorical Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 4 Segment((x: 0.0 m), (x: 1.0 m))
3 6 Segment((x: 2.0 m), (x: 3.0 m))

The DropNaN is an alternative to drop all rows for which the selected column values are NaN.

5.2.3 Statistical

The framework provides various feature transforms for statistical analysis. We will cover some of these transforms in more detail in Part V of the book with real data. In the following examples we illustrate the most basic statistical transforms with synthetic data.

The Sample transform can be used to sample rows of the geotable at random, with or without replacement depending on the replace option. Other options are available such as rng to set the random number generator and ordered to preserve the order of rows in the original geotable:

gt |> Sample(1000, replace=false) |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238
Note

Similar to Filter and Sort, the Sample transform is lazy. It simply stores the indices of sampled rows for future construction of the new geotable.

The Center and Scale transforms can be used to standardize the range of values in a geotable. Aliases are provided for specific types of Scale such as MinMax and Interquartile. We can use the describe function to visualize basic statistics before and after the transforms:

gt |> describe
Table with 6 columns and 4 rows:
     variable  mean         minimum   median       maximum  nmissing
   ┌────────────────────────────────────────────────────────────────
 1 │ a         2.99936      -3.65899  1.98811      12.8392  0
 2 │ b         0.00810625   -11.7646  0.00128202   11.1792  0
 3 │ c         -0.00961677  -3.82101  -0.0093037   4.37153  0
 4 │ d         -0.00294808  -4.58929  -0.00314577  4.94433  0
gt |> Center("a") |> describe
Table with 6 columns and 4 rows:
     variable  mean         minimum   median       maximum  nmissing
   ┌────────────────────────────────────────────────────────────────
 1 │ a         3.63798e-16  -6.65835  -1.01125     9.83982  0
 2 │ b         0.00810625   -11.7646  0.00128202   11.1792  0
 3 │ c         -0.00961677  -3.82101  -0.0093037   4.37153  0
 4 │ d         -0.00294808  -4.58929  -0.00314577  4.94433  0
gt |> MinMax() |> describe
Table with 6 columns and 4 rows:
     variable  mean      minimum  median    maximum  nmissing
   ┌─────────────────────────────────────────────────────────
 1 │ a         0.403581  0.0      0.342286  1.0      0
 2 │ b         0.513111  0.0      0.512814  1.0      0
 3 │ c         0.465227  0.0      0.465266  1.0      0
 4 │ d         0.48107   0.0      0.481049  1.0      0

The ZScore transform is similar to the Scale transform, but it uses the mean and the standard deviation to standardize the range:

gt |> ZScore() |> describe
Table with 6 columns and 4 rows:
     variable  mean          minimum   median        maximum  nmissing
   ┌──────────────────────────────────────────────────────────────────
 1 │ a         0.0           -1.97514  -0.299978     2.91889  0
 2 │ b         -5.68434e-18  -4.60994  -0.00267223   4.37436  0
 3 │ c         -1.06581e-18  -3.82162  0.000313906   4.3929   0
 4 │ d         5.06262e-18   -3.92526  -0.000169194  4.23418  0

Another important univariate transform is the Quantile transform, which can be used to convert empirical distribution in a column of the geotable to any given distribution from Distributions.jl by Lin et al. (2023). Selected columns are converted to a Normal distribution by default, but more than 60 distributions are available:

gt |> Quantile() |> values |> pairplot
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

In data science, scientific traits are used to link data types to adequate statistical algorithms. The most popular scientific traits encountered in geoscientific applications are the Continuous and the Categorical scientific traits. To convert (or coerce) the scientific traits of columns in a geotable, we can use the Coerce transform:

st = georef((a=[1,2,2,2,3,3], b=[1,2,3,4,5,6])) |> Coerce("b" => Continuous)
6×3 GeoTable over 6 CartesianGrid
a b geometry
Categorical Continuous Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 1.0 Segment((x: 0.0 m), (x: 1.0 m))
2 2.0 Segment((x: 1.0 m), (x: 2.0 m))
2 3.0 Segment((x: 2.0 m), (x: 3.0 m))
2 4.0 Segment((x: 3.0 m), (x: 4.0 m))
3 5.0 Segment((x: 4.0 m), (x: 5.0 m))
3 6.0 Segment((x: 5.0 m), (x: 6.0 m))
eltype(st.b)
Float64
Note

All scientific traits are documented in the DataScienceTraits.jl module, and can be used to select variables:

st |> Only(Continuous)
6×2 GeoTable over 6 CartesianGrid
b geometry
Continuous Segment
[NoUnits] 🖈 Cartesian{NoDatum}
1.0 Segment((x: 0.0 m), (x: 1.0 m))
2.0 Segment((x: 1.0 m), (x: 2.0 m))
3.0 Segment((x: 2.0 m), (x: 3.0 m))
4.0 Segment((x: 3.0 m), (x: 4.0 m))
5.0 Segment((x: 4.0 m), (x: 5.0 m))
6.0 Segment((x: 5.0 m), (x: 6.0 m))

The Levels transform can be used to adjust the categories (or levels) of Categorical columns in case the sampling process does not include all possible values:

st = st |> Levels("a" => [1,2,3,4])
6×3 GeoTable over 6 CartesianGrid
a b geometry
Categorical Continuous Segment
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 1.0 Segment((x: 0.0 m), (x: 1.0 m))
2 2.0 Segment((x: 1.0 m), (x: 2.0 m))
2 3.0 Segment((x: 2.0 m), (x: 3.0 m))
2 4.0 Segment((x: 3.0 m), (x: 4.0 m))
3 5.0 Segment((x: 4.0 m), (x: 5.0 m))
3 6.0 Segment((x: 5.0 m), (x: 6.0 m))
levels(st.a)
4-element Vector{Int64}:
 1
 2
 3
 4

Another popular transform in statistical learning is the OneHot transform. It converts a Categorical column into multiple columns of true/false values, one column for each level:

st |> OneHot("a")
6×6 GeoTable over 6 CartesianGrid
a_1 a_2 a_3 a_4 b geometry
Categorical Categorical Categorical Categorical Continuous Segment
[NoUnits] [NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
true false false false 1.0 Segment((x: 0.0 m), (x: 1.0 m))
false true false false 2.0 Segment((x: 1.0 m), (x: 2.0 m))
false true false false 3.0 Segment((x: 2.0 m), (x: 3.0 m))
false true false false 4.0 Segment((x: 3.0 m), (x: 4.0 m))
false false true false 5.0 Segment((x: 4.0 m), (x: 5.0 m))
false false true false 6.0 Segment((x: 5.0 m), (x: 6.0 m))

A similar transform for Continuous columns is the Indicator transform. It converts the column into multiple columns based on threshold values on the support of the data. By default, the threshold values are computed on a quantile scale:

st |> Indicator("b", k=3, scale=:quantile)
6×5 GeoTable over 6 CartesianGrid
a b_1 b_2 b_3 geometry
Categorical Categorical Categorical Categorical Segment
[NoUnits] [NoUnits] [NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 true true true Segment((x: 0.0 m), (x: 1.0 m))
2 true true true Segment((x: 1.0 m), (x: 2.0 m))
2 false true true Segment((x: 2.0 m), (x: 3.0 m))
2 false true true Segment((x: 3.0 m), (x: 4.0 m))
3 false false true Segment((x: 4.0 m), (x: 5.0 m))
3 false false true Segment((x: 5.0 m), (x: 6.0 m))

More advanced statistical transforms such as EigenAnalysis, PCA, DRS, SDS, ProjectionPursuit for multivariate data analysis and Remainder, Closure, LogRatio, ALR, CLR, ILR for compositional data analysis will be covered in future chapters.

5.3 Geometric transforms

While feature transforms operate on the values of the geotable, geometric transforms operate on the geospatial domain. The framework provides various geometric transforms for 2D and 3D space.

5.3.1 Coordinate

A coordinate transform is a geometric transform that modifies the coordinates of all points in the domain without any advanced topological modification (i.e., connectivities are preserved). The most prominent examples of coordinate transforms are Translate, Rotate and Scale.

Let’s load an additional geotable to see these transforms in action:

using GeoIO

bt = GeoIO.load("data/beethoven.ply")

viz(bt.geometry)
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

The Beethoven domain has been saved in the .ply file in a position that is not ideal for visualization. We can rotate this domain with any active rotation specification from Rotations.jl by Koolen et al. (2023) to improve the visualization. For example, we can specify that we want to rotate all points in the mesh by analogy with a rotation between coordinates (0, 1, 0) and coordinates (0, 0, 1):

rt = bt |> Rotate((0, 1, 0), (0, 0, 1))

viz(rt.geometry)
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

Beethoven is now standing up, but still facing the wall. Let’s rotate it once again by analogy between coordinates (1, 0, 0) and (-1, 1, 0):

rt = rt |> Rotate((1, 0, 0), (-1, 1, 0))

viz(rt.geometry)
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

Rotation specifications are also available in 2D space. As an example, we can rotate the 2D grid of our synthetic geotable by the counter clockwise angle π/4:

gt |> Rotate(Angle2d(π/4)) |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

In GIS, this new geotable would be called a rotated “raster”. As another example, let’s translate the geotable to the origin of the coordinate system with the Translate transform:

c = centroid(gt.geometry)

gt |> Translate(-to(c)...) |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

and scale it with a positive factor for each dimension:

gt |> Scale(0.1, 0.2) |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

The StdCoords transform combines Translate and Scale to standardize the coordinates of the domain to the interval [-0.5, 0.5]:

gt |> StdCoords() |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

In GIS, another very important coordinate transform is the Proj transform. We will cover this transform in the next chapter because it depends on the concept of map projection, which deserves more attention.

Note

In our framework, the Proj transform is just another coordinate transform. It is implemented with the same code optimizations, and can be used in conjunction with many other transforms that are not available elsewhere.

5.3.2 Advanced

Advanced geometric transforms are provided that change the topology of the domain besides the coordinates of points. Some of these transforms can be useful to repair problematic geometries acquired from sensors in the real world.

The Repair transform is parameterized by an integer K that identifies the repair to be performed. For example, Repair{0}() is a transform that removes duplicated vertices and faces in a domain represented by a mesh. The Repair{9}() on the other hand fixes the orientation of rings in polygonal areas so that the external boundary is oriented counter clockwise and the inner boundaries are oriented clockwise. The list of available repairs will continue to grow with the implementation of new geometric algorithms in the framework.

To understand why geometric transforms are more general than coordinate transforms, let’s consider the following polygonal area with holes:

outer = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
hole1 = [(0.2, 0.2), (0.2, 0.4), (0.4, 0.4), (0.4, 0.2)]
hole2 = [(0.6, 0.2), (0.6, 0.4), (0.8, 0.4), (0.8, 0.2)]
poly  = PolyArea([outer, hole1, hole2])

viz(poly)
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

We can connect the holes with the external boundary (or ring) using the Bridge transform:

poly |> Bridge(0.01) |> viz
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

By looking at the visualization, we observe that the number of vertices changed to accommodate the so called “bridges” between the rings. The topology also changed as there are no holes in the resulting geometry.

As a final example of advanced geometric transform, we illustrate the TaubinSmoothing transform, which gradually removes sharp boundaries of a manifold mesh:

st = bt |> TaubinSmoothing(30)

fig = Mke.Figure()
viz(fig[1,1], bt.geometry)
viz(fig[1,2], st.geometry)
fig
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

For more advanced geometric transforms, please consult the official documentation.

5.4 Geospatial transforms

Geospatial transforms are those transforms that operate on both the values and the domain of the geotable. They are common in geostatistical workflows that need to remove geospatial “trends” or workflows that need to extract geometries from domains.

As an example, let’s consider the following geotable with a variable z that made of a trend component μ and a noise component ϵ:

# quadratic + noise
r = range(-1, stop=1, length=100)
μ = [x^2 + y^2 for x in r, y in r]
ϵ = 0.1rand(100, 100)
t = georef((z=μ+ϵ,))

viewer(t)
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

We can use the Detrend transform to remove a trend of polynomial degree 2:

t |> Detrend(degree=2) |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

The remaining component can then be modeled with geostatistical models of geospatial correlation, which will be covered in Part IV of the book.

Models of geospatial correlation such as variograms (Hoffimann and Zadrozny 2019) require unique coordinates in the geotable and that is the purpose of the UniqueCoords transform. It removes duplicate points in the geotable and aggregates the values with custom aggregation functions.

Let’s consider the following geotable stored in a .png file to illustrate another geospatial transform:

letters = GeoIO.load("data/letters.png")
44255×2 GeoTable over 265×167 TransformedGrid
color geometry
Colorful Quadrangle
[NoUnits] 🖈 Cartesian{NoDatum}
Gray{N0f8}(1.0) Quadrangle((x: -1.62266e-14 m, y: 265.0 m), ..., (x: 1.0 m, y: 265.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.61653e-14 m, y: 264.0 m), ..., (x: 1.0 m, y: 264.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.61041e-14 m, y: 263.0 m), ..., (x: 1.0 m, y: 263.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.60429e-14 m, y: 262.0 m), ..., (x: 1.0 m, y: 262.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.59816e-14 m, y: 261.0 m), ..., (x: 1.0 m, y: 261.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.59204e-14 m, y: 260.0 m), ..., (x: 1.0 m, y: 260.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.58592e-14 m, y: 259.0 m), ..., (x: 1.0 m, y: 259.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.57979e-14 m, y: 258.0 m), ..., (x: 1.0 m, y: 258.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.57367e-14 m, y: 257.0 m), ..., (x: 1.0 m, y: 257.0 m))
Gray{N0f8}(1.0) Quadrangle((x: -1.56755e-14 m, y: 256.0 m), ..., (x: 1.0 m, y: 256.0 m))

The Potrace transform can be used to extract complex geometries from a geotable over a 2D Grid. It transforms the Grid domain into a GeometrySet based on any column that contains a discrete set of marker values. In this example, we use the color as the column with markers:

Ab = letters |> Potrace("color", ϵ=0.8)
2×2 GeoTable over 2 GeometrySet
color geometry
Colorful MultiPolygon
[NoUnits] 🖈 Cartesian{NoDatum}
Gray{N0f8}(1.0) Multi(4×PolyArea)
Gray{N0f8}(0.0) Multi(2×PolyArea)

The option ϵ controls the deviation tolerance used to simplify the boundaries of the geometries. The higher is the tolerance, the less is the number of segments in the boundary:

viz(Ab.geometry[2], color = "black")
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

In the reverse direction, we have the Rasterize transform, which takes a geotable over a GeometrySet and assigns the geometries to a Grid. In this transform, we can either provide an external grid for the the assignments, or request a grid size to discretize the boundingbox of all geometries:

A = [1, 2, 3, 4, 5]
B = [1.1, 2.2, 3.3, 4.4, 5.5]
p1 = Triangle((2, 0), (6, 2), (2, 2))
p2 = Triangle((0, 6), (3, 8), (0, 10))
p3 = Quadrangle((3, 6), (9, 6), (9, 9), (6, 9))
p4 = Quadrangle((7, 0), (10, 0), (10, 4), (7, 4))
p5 = Pentagon((1, 3), (5, 3), (6, 6), (3, 8), (0, 6))
gt = georef((; A, B), [p1, p2, p3, p4, p5])
5×3 GeoTable over 5 GeometrySet
A B geometry
Categorical Continuous Ngon
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
1 1.1 Triangle((x: 2.0 m, y: 0.0 m), (x: 6.0 m, y: 2.0 m), (x: 2.0 m, y: 2.0 m))
2 2.2 Triangle((x: 0.0 m, y: 6.0 m), (x: 3.0 m, y: 8.0 m), (x: 0.0 m, y: 10.0 m))
3 3.3 Quadrangle((x: 3.0 m, y: 6.0 m), ..., (x: 6.0 m, y: 9.0 m))
4 4.4 Quadrangle((x: 7.0 m, y: 0.0 m), ..., (x: 7.0 m, y: 4.0 m))
5 5.5 Pentagon((x: 1.0 m, y: 3.0 m), ..., (x: 0.0 m, y: 6.0 m))
gt |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238
nt = gt |> Rasterize(20, 20)
400×3 GeoTable over 20×20 CartesianGrid
A B geometry
Categorical Continuous Quadrangle
[NoUnits] [NoUnits] 🖈 Cartesian{NoDatum}
missing missing Quadrangle((x: 0.0 m, y: 0.0 m), ..., (x: 0.0 m, y: 0.5 m))
missing missing Quadrangle((x: 0.5 m, y: 0.0 m), ..., (x: 0.5 m, y: 0.5 m))
missing missing Quadrangle((x: 1.0 m, y: 0.0 m), ..., (x: 1.0 m, y: 0.5 m))
1 1.1 Quadrangle((x: 1.5 m, y: 0.0 m), ..., (x: 1.5 m, y: 0.5 m))
1 1.1 Quadrangle((x: 2.0 m, y: 0.0 m), ..., (x: 2.0 m, y: 0.5 m))
missing missing Quadrangle((x: 2.5 m, y: 0.0 m), ..., (x: 2.5 m, y: 0.5 m))
missing missing Quadrangle((x: 3.0 m, y: 0.0 m), ..., (x: 3.0 m, y: 0.5 m))
missing missing Quadrangle((x: 3.5 m, y: 0.0 m), ..., (x: 3.5 m, y: 0.5 m))
missing missing Quadrangle((x: 4.0 m, y: 0.0 m), ..., (x: 4.0 m, y: 0.5 m))
missing missing Quadrangle((x: 4.5 m, y: 0.0 m), ..., (x: 4.5 m, y: 0.5 m))
nt |> viewer
┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
└ @ Makie ~/.julia/packages/Makie/Q6F2P/src/scenes.jl:238

The values of the variables are aggregated at geometric intersections using a default aggregation function, which can be overwritten with an option. Once the geotable is defined over a Grid, it is possible to refine or coarsen the grid with the Downscale and Upscale transforms.

The Transfer of values to a new geospatial domain is another very useful geospatial transform. The Aggregate transform is related, but aggregates the values with given aggregation functions.

5.5 Remarks

In this chapter we learned the important concept of transforms, and saw examples of the concept in action with synthetic data. In order to leverage the large number of transforms implemented in the framework, all we need to do is load our geospatial data as a geotable using georef or GeoIO.jl.

Some additional remarks:

  • One of the major advantages of transforms compared to traditional row/column access in data science is that they preserve geospatial information. There is no need to keep track of indices in arrays to repeatedly reattach values to geometries.
  • Transforms can be organized into three classes—feature, geometric and geospatial—depending on how they operate with the values and the domain of the geotable:
    • Feature transforms operate on the values. They include column selection, data cleaning, statistical analysis and any transform designed for traditional Tables.jl.
    • Geometric transforms operate on the domain. They include coordinate transforms that simply modify the coordinates of points as well as more advanced transforms that can change the topology of the domain.
    • Geospatial transforms operate on both the values and domain. They include geostatistical transforms and transforms that use other columns besides the geometry column to produce new columns and geometries.

In the next chapters, we will review map projections with the Proj coordinate transform, and will introduce one of the greatest features of the framework known as transform pipelines.